<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>在线YUV文件查看器</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
    <style>
        body {
            font-family: 'Inter', 'Helvetica Neue', 'Arial', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
            background-color: #111827;
            color: #E5E7EB;
        }
        .container {
            max-width: 1200px;
        }
        .control-panel {
            background-color: #1F2937;
            border: 1px solid #374151;
        }
        .canvas-container {
            background-color: #000;
            border: 1px solid #374151;
        }
        .drop-zone {
            border: 2px dashed #4B5563;
        }
        .drop-zone.drag-over {
            border-color: #3B82F6;
            background-color: #1E3A8A;
        }
        .btn {
            background-color: #2563EB;
            transition: background-color 0.3s;
        }
        .btn:hover:not(:disabled) {
            background-color: #1D4ED8;
        }
        .btn:disabled {
            background-color: #4B5563;
            cursor: not-allowed;
        }
    </style>
</head>
<body class="w-full min-h-screen flex items-center justify-center p-4">

    <div class="container w-full mx-auto flex flex-col lg:flex-row gap-8">
        
        <!-- 控制面板 -->
        <div class="control-panel w-full lg:w-1/3 p-6 rounded-xl shadow-lg">
            <h1 class="text-2xl font-bold text-white mb-2">YUV 查看器</h1>
            <p class="text-sm text-gray-400 mb-6">上传YUV文件，设置参数后即可预览。</p>

            <div id="drop-zone" class="drop-zone w-full p-6 text-center rounded-lg cursor-pointer hover:bg-gray-700 transition">
                <p id="drop-text" class="text-gray-400">将YUV文件拖拽至此<br>或点击选择文件</p>
                <input type="file" id="file-input" class="hidden">
            </div>
            <p id="file-name" class="text-center text-sm text-green-400 mt-2 h-5"></p>

            <div class="grid grid-cols-2 gap-4 mt-6">
                <div>
                    <label for="width" class="block text-sm font-medium text-gray-300">宽度 (Width)</label>
                    <input type="number" id="width" value="1920" class="mt-1 block w-full bg-gray-900 border border-gray-600 rounded-md shadow-sm py-2 px-3 text-white focus:outline-none focus:ring-blue-500 focus:border-blue-500">
                </div>
                <div>
                    <label for="height" class="block text-sm font-medium text-gray-300">高度 (Height)</label>
                    <input type="number" id="height" value="1080" class="mt-1 block w-full bg-gray-900 border border-gray-600 rounded-md shadow-sm py-2 px-3 text-white focus:outline-none focus:ring-blue-500 focus:border-blue-500">
                </div>
            </div>

            <div class="mt-4">
                <label for="format" class="block text-sm font-medium text-gray-300">像素格式 (Pixel Format)</label>
                <select id="format" class="mt-1 block w-full bg-gray-900 border border-gray-600 rounded-md shadow-sm py-2 px-3 text-white focus:outline-none focus:ring-blue-500 focus:border-blue-500">
                    <option value="I420">I420 (Planar YUV 4:2:0)</option>
                    <option value="NV12">NV12 (Semi-Planar YUV 4:2:0)</option>
                    <option value="YUYV">YUYV / YUY2 (Packed YUV 4:2:2)</option>
                    <option value="UYVY">UYVY (Packed YUV 4:2:2)</option>
                </select>
            </div>
            
            <div class="mt-4">
                <label for="frame-index" class="block text-sm font-medium text-gray-300">帧序号 (Frame Index)</label>
                <input type="number" id="frame-index" value="0" min="0" class="mt-1 block w-full bg-gray-900 border border-gray-600 rounded-md shadow-sm py-2 px-3 text-white focus:outline-none focus:ring-blue-500 focus:border-blue-500">
            </div>

            <button id="view-btn" class="btn w-full text-white font-bold py-2 px-4 rounded-lg mt-8" disabled>
                预览图像
            </button>
            
            <div id="status" class="mt-4 text-center text-yellow-400 h-5"></div>
        </div>

        <!-- 图像显示区 -->
        <div class="canvas-container w-full lg:w-2/3 flex items-center justify-center p-4 rounded-xl shadow-lg">
            <canvas id="canvas"></canvas>
        </div>

    </div>

    <script>
        const dropZone = document.getElementById('drop-zone');
        const fileInput = document.getElementById('file-input');
        const viewBtn = document.getElementById('view-btn');
        const widthInput = document.getElementById('width');
        const heightInput = document.getElementById('height');
        const formatSelect = document.getElementById('format');
        const frameIndexInput = document.getElementById('frame-index');
        const canvas = document.getElementById('canvas');
        const ctx = canvas.getContext('2d');
        const statusEl = document.getElementById('status');
        const fileNameEl = document.getElementById('file-name');
        const dropTextEl = document.getElementById('drop-text');

        let fileBuffer = null;

        // 文件选择逻辑
        dropZone.addEventListener('click', () => fileInput.click());
        fileInput.addEventListener('change', (e) => handleFile(e.target.files[0]));

        // 拖拽逻辑
        dropZone.addEventListener('dragover', (e) => {
            e.preventDefault();
            dropZone.classList.add('drag-over');
        });
        dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
        dropZone.addEventListener('drop', (e) => {
            e.preventDefault();
            dropZone.classList.remove('drag-over');
            handleFile(e.dataTransfer.files[0]);
        });

        function handleFile(file) {
            if (!file) return;
            const reader = new FileReader();
            reader.onload = (e) => {
                fileBuffer = e.target.result;
                viewBtn.disabled = false;
                fileNameEl.textContent = file.name;
                dropTextEl.textContent = '文件已加载';
            };
            reader.onerror = () => {
                statusEl.textContent = '错误：无法读取文件。';
                fileBuffer = null;
                viewBtn.disabled = true;
            };
            reader.readAsArrayBuffer(file);
        }

        viewBtn.addEventListener('click', renderYUV);

        function clamp(value) {
            return Math.max(0, Math.min(255, value));
        }

        function renderYUV() {
            if (!fileBuffer) {
                statusEl.textContent = '请先选择一个YUV文件。';
                return;
            }

            const width = parseInt(widthInput.value);
            const height = parseInt(heightInput.value);
            const format = formatSelect.value;
            const frameIndex = parseInt(frameIndexInput.value);

            if (isNaN(width) || isNaN(height) || width <= 0 || height <= 0) {
                statusEl.textContent = '请输入有效的宽度和高度。';
                return;
            }

            canvas.width = width;
            canvas.height = height;

            const imageData = ctx.createImageData(width, height);
            const data = imageData.data;
            const buffer = new Uint8Array(fileBuffer);
            
            let frameSize = 0;
            switch(format) {
                case 'I420':
                case 'NV12':
                    frameSize = width * height * 1.5;
                    break;
                case 'YUYV':
                case 'UYVY':
                    frameSize = width * height * 2;
                    break;
            }

            const frameOffset = frameSize * frameIndex;
            
            if (buffer.length < frameOffset + frameSize) {
                statusEl.textContent = `错误：文件大小不足以显示第 ${frameIndex} 帧。`;
                return;
            }
            
            statusEl.textContent = '';


            // YUV to RGB conversion
            for (let y = 0; y < height; y++) {
                for (let x = 0; x < width; x++) {
                    const pixelIndex = (y * width + x) * 4;
                    let Y, U, V;

                    if (format === 'I420') {
                        const yIndex = frameOffset + y * width + x;
                        const uIndex = frameOffset + width * height + Math.floor(y / 2) * Math.floor(width / 2) + Math.floor(x / 2);
                        const vIndex = frameOffset + width * height + (width * height / 4) + Math.floor(y / 2) * Math.floor(width / 2) + Math.floor(x / 2);
                        Y = buffer[yIndex];
                        U = buffer[uIndex];
                        V = buffer[vIndex];
                    } else if (format === 'NV12') {
                        const yIndex = frameOffset + y * width + x;
                        const uvIndex = frameOffset + width * height + Math.floor(y / 2) * width + Math.floor(x / 2) * 2;
                        Y = buffer[yIndex];
                        U = buffer[uvIndex];
                        V = buffer[uvIndex + 1];
                    } else if (format === 'YUYV') {
                        const yuvIndex = frameOffset + (y * width + x) * 2;
                        Y = buffer[yuvIndex];
                        U = buffer[yuvIndex - (x % 2) + 1];
                        V = buffer[yuvIndex + (x % 2) + 1];
                    } else if (format === 'UYVY') {
                         const yuvIndex = frameOffset + (y * width + x) * 2;
                        Y = buffer[yuvIndex + 1];
                        U = buffer[yuvIndex - (x % 2)];
                        V = buffer[yuvIndex + (x % 2)];
                    }

                    // Conversion formula
                    const C = Y - 16;
                    const D = U - 128;
                    const E = V - 128;

                    data[pixelIndex]     = clamp((298.082 * C + 408.583 * E) / 256); // R
                    data[pixelIndex + 1] = clamp((298.082 * C - 100.291 * D - 208.120 * E) / 256); // G
                    data[pixelIndex + 2] = clamp((298.082 * C + 516.412 * D) / 256); // B
                    data[pixelIndex + 3] = 255; // A
                }
            }

            ctx.putImageData(imageData, 0, 0);
        }
    </script>
</body>
</html>
